(ns jumpluff.poi
  (:require [cljs.core.async :as async]
            [clojure.string :as s]
            [jumpluff.state :as state]
            [jumpluff.utils :refer [vecs->maps
                                    dataset-names
                                    dataset-names-some-exact-match]]))

(def fuse (js/require "fuse.js"))

(defn- fuse-search-clj->js->clj
  [opts query collection]
  (js->clj (.search (fuse. (clj->js collection)
                           (clj->js opts))
                    query)))

(defn- fuse-search
  "Use fuse.js to fuzzy match a collection.
  The collection will go through `clj->js` and `js->clj`, losing
  keywords to strings."
  ([query collection]
   (when (every? string? collection)
     (when-let [vec-idx
                (not-empty
                 (fuse-search-clj->js->clj {:shouldSort true
                                            :threshold 0.3
                                            :location 0
                                            :distance 100
                                            :maxPatternLength 32
                                            :minMatchCharLength 2
                                            :tokenize true :matchAllTokens true}
                                           query collection))]
       (map #(nth collection %) vec-idx))))
  ;; with a custom getfn like nth to search by index
  ([keys getfn coll query]
   (fuse-search-clj->js->clj
    {:shouldSort true
     :threshold 0.2
     :location 0
     :distance 100
     :maxPatternLength 32
     :minMatchCharLength 2
     :tokenize true :matchAllTokens true
     :keys keys
     :getFn getfn}
    query coll)))

(defn- fuse-search--restore-keywords
  "Restore the :ex column to a keyword from a string"
  [js-vecs {:keys [index-ex]}]
  (map (fn [vec]
         (if (int? index-ex)
           (assoc vec index-ex
                  (keyword (nth vec index-ex)))
           vec))
       js-vecs))

(defn- split-query
  "Convoluted parser
  dataset name : name filter - description filter (-ex|+ex|+ex-t)"
  [query]
  (let [[query ex-keyword]
        (or (and (s/ends-with? query "+ex-t")
                 [(s/replace query #"\+ex-t$" "")
                  :ex-triggered])
            (and (s/ends-with? query "+ex")
                 [(s/replace query #"\+ex$" "")
                  :ex-possible])
            (and (s/ends-with? query "-ex")
                 [(s/replace query #"-ex$" "")
                  false])
            [query nil])]
    (let [split-on-colon (s/split query #":" 2)
          split-on-dash (s/split (last split-on-colon) #"-" -1)
          dataset-query (if (< 1 (count split-on-colon))
                          (s/trim (first split-on-colon)))
          name-query (cond (= 1 (count split-on-dash)) (s/trim (first split-on-dash))
                           (< 1 (count split-on-dash))
                           (s/trim (s/join "-" (drop-last split-on-dash)))
                           :else "")
          description-query (if-not (= 1 (count split-on-dash))
                              (s/trim (or (last split-on-dash) "")))]
      [dataset-query
       name-query
       description-query
       ex-keyword])))

(defn- poi-filter-by-nickname
  [name columns vec-collection]
  (when (int? (:index-nicknames columns))
    (let [nick (s/lower-case name)]
      (filter #(some #{nick} (nth % (:index-nicknames columns)))
              vec-collection))))

(defn s-replace-recur
  [st coll]
  (if (not-empty coll)
    (let [[re replacement] (first (take 1 coll))]
      (s-replace-recur (s/replace st re replacement)
                       (drop 1 coll)))
    st))

(defn sanitize-abbrv-map
  "Transform a map of keywords to strings to a nested vector of
  sanitized regexps to sanitized replacements."
  [m]
  (->> m
     (map (fn [[abbrv expansion]]
            (let [filter-str #(apply str (re-seq %1 (apply str (take 200 %2))))
                  re (when-let [abbrv-alpha (filter-str #"[a-zA-Z]" (name abbrv))]
                       (when (not-empty abbrv-alpha)
                         (re-pattern (str "(?i)\\b" abbrv-alpha "\\b"))))
                  replacement (not-empty (filter-str #"[\w\s\d]" (name expansion)))]
              (when (and re replacement)
                [re replacement]))))
     (remove nil?)))

(defn poi-name-abbrvs
  [name dataset-name]
  (or (some->> dataset-name
               (get @state/dataset-descriptions)
               :abbrv
               sanitize-abbrv-map
               (s-replace-recur name))
      name))

(defn- poi-filter-by--fuse
  [query filter-each index-key columns vec-collection]
  (when (int? (index-key columns))
    (fuse-search--restore-keywords
     (fuse-search [{:name (index-key columns)}]
                  #(filter-each (nth %1 %2 ""))
                  vec-collection
                  (filter-each query))
     columns)))

(defn- poi-filter-by-name
  [name columns vec-collection]
  (poi-filter-by--fuse name
                       #(s/replace % #"-|'|\." "")
                       :index-name
                       columns
                       vec-collection))

(defn- poi-filter-by-description
  [description-query columns vec-collection]
  (poi-filter-by--fuse description-query
                       #(s/replace % #"(?i)(?:\band\b|&)" "")
                       :index-description
                       columns
                       vec-collection))

(defn- poi-filter-by-ex
  [ex-keyword columns vec-collection]
  (when (int? (:index-ex columns))
    (filter (cond (= :ex-triggered ex-keyword)
                  #(= ex-keyword (nth % (:index-ex columns)))
                  (keyword? ex-keyword)
                  #(nth % (:index-ex columns))
                  :else
                  #(not (nth % (:index-ex columns))))
            vec-collection)))

(defn poi-search
  "Filter points of interest by QUERY.
  Use DATASET-NAME-FALLBACK to find a collection to filter if one was
  not named in the query.  When unable to find a collection the result
  will be nil, otherwise if nothing was found the result will be an
  empty vector."
  [dataset-name-fallback query]
  (let [[dataset-query name-query description-query ex-keyword] (split-query (or query ""))
        dataset-name (or (when (not-empty dataset-query)
                           (first (fuse-search dataset-query (dataset-names))))
                         dataset-name-fallback)]
    (when (dataset-names-some-exact-match dataset-name)
      (let [conf (get @state/dataset-descriptions dataset-name)
            vec-coll (get @state/csv-files (:source conf))
            columns (:columns conf)
            search-split-by-re
            (fn [re query coll]
              (when (re-find re query)
                (let [[name desc] (map s/trim (s/split query re))]
                  (when (and (not-empty name) (not-empty desc))
                    (some->> (not-empty (or (not-empty (poi-filter-by-nickname name columns coll))
                                            (not-empty (poi-filter-by-name
                                                        (poi-name-abbrvs name dataset-name) columns coll))))
                             (poi-filter-by-description desc columns))))))]
        (vecs->maps
         {:name (:index-name columns)
          :gps (:index-gps columns)
          :description (:index-description columns)
          :ex (:index-ex columns)
          :nicknames (:index-nicknames columns)}
         (cond->> vec-coll
           ;; name
           (not-empty name-query)
           ((fn [<>]
              (or (not-empty (poi-filter-by-nickname name-query columns <>))
                  (not-empty (poi-filter-by-name (poi-name-abbrvs name-query dataset-name) columns <>))
                  ;; Fallback: If description-query is empty run poi-filter-by-description with name-query
                  ;; if still nothing, try searching nick/name and description split by 'on'
                  (when (not (not-empty description-query))
                    (or (not-empty (poi-filter-by-description name-query columns <>))
                        (not-empty (search-split-by-re #"(?i)\bon\b" name-query <>))
                        (search-split-by-re #"(?i)\bat\b" name-query <>)))
                  [])))
           ;; desc
           (not-empty description-query)
           (poi-filter-by-description description-query columns)
           ;; ex keywords
           (not (nil? ex-keyword))
           (poi-filter-by-ex ex-keyword columns)))))))
